// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: taekwon

#pragma once

#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utility>

#include "coin2/exchange/base/log/strategy_logger.h"

namespace coin2::strategy::util {

using coin2::exchange::base::strategy_util::StrategyLogger;
using coin2::exchange::base::strategy_util::StrategySlimLogger;
using coin2::exchange::base::strategy_util::TelemetryLogger;
using coin2::exchange::base::symbology::IProduct;

class StrategyReporter {
 public:
  typedef std::function<
      void(const coin::proto::StrategyLog&, bool, int64_t)> log_callback_type;

  StrategyReporter(
      const StrategyManagerConfig& strategy_config,
      const std::unordered_map<std::string, double>& reserve_map)
      : strategy_config_(strategy_config),
        reserve_map_(reserve_map),
        prev_pnl_send_time_(0),
        pnl_interval_(20'000'000'000LL) {
    // Strategy Request
    coin::proto::StrategyRequestProto strat_request;
    strat_request.set_strategy_group(strategy_config.strategy_group());
    strat_request.set_strategy_name(strategy_config.strategy_name());
    const auto& strat_slim_logger_config = strategy_config.strategy_slim_logger_config();
    const auto& strat_logger_config = strategy_config.strategy_logger_config();
    const auto& telemetry_logger_config = strategy_config.telemetry_logger_config();
    strat_slim_logger_ = std::make_unique<StrategySlimLogger>(
        strat_request,
        strat_slim_logger_config.log_root(),
        strat_slim_logger_config.machine());
    strat_logger_ = std::make_unique<StrategyLogger>(
        strat_request,
        strat_logger_config.log_root(),
        strat_logger_config.machine());
    telemetry_logger_ = std::make_unique<TelemetryLogger>(
        strat_request,
        telemetry_logger_config.log_root(),
        telemetry_logger_config.machine());
    log_callback_type strat_slim_log_write_callback = std::bind(
            &StrategySlimLogger::CondWrite,
            strat_slim_logger_.get(),
            std::placeholders::_1,
            std::placeholders::_2,
            std::placeholders::_3);
    strat_logger_->AddCallback(strat_slim_log_write_callback);
    log_callback_type telemetry_log_write_callback = std::bind(
        &TelemetryLogger::CondWrite,
        telemetry_logger_.get(),
        std::placeholders::_1,
        std::placeholders::_2,
        std::placeholders::_3);
    strat_logger_->AddCallback(telemetry_log_write_callback);

    auto curr_time = GetCurrentTimestamp();
    strat_logger_->WriteRequest(true, curr_time);

    // Reserve Map
    for (const auto& [currency, reserve] : reserve_map_) {
      auto* currency_balance = acc_balance_for_reserve_.add_each_balance();
      currency_balance->set_currency(currency);
      currency_balance->set_total(reserve);
    }
    if (acc_balance_for_reserve_.each_balance_size() > 0) {
      strat_logger_->WriteReserve(acc_balance_for_reserve_, true, curr_time);
    }

    auto interval = strategy_config.pnl_report_period_ns();
    pnl_interval_ = (interval < pnl_interval_) ? interval : pnl_interval_;
  }

  std::map<std::string, CurrencyBalance*> cbal_ptr;

  void UpdateReserve(int64_t curr_time, const std::string& base, double reserve) {
    reserve_map_[base] = reserve;
    if (cbal_ptr.count(base) == 0) {
      auto* currency_balance = acc_balance_for_reserve_.add_each_balance();
      currency_balance->set_currency(base);
      currency_balance->set_total(reserve);
      cbal_ptr[base] = currency_balance;
    }
    cbal_ptr.at(base)->set_total(reserve);
  }

  StrategyLogger* GetMutableStrategyLogger() { return strat_logger_.get(); }

  void RegisterProduct(const IProduct& product) {
    // CHECK_EQ(++base_ccy_count_[product.base().currency()], 1);
    ++base_ccy_count_[product.quote().currency()];
    ++quote_ccy_count_[product.quote().currency()];
    if (quote_ccy_count_.size() > 1) {
      CHECK_LE(quote_ccy_count_.size(), 2) << "3 quotes are not allowed";
      // if A-BTC & BTC-USDT exists,
      // convert A-BTC to A-USDT & think.
      // let's only allow -BTC. that'll be enough.
      CHECK_EQ(quote_ccy_count_.count("BTC"), 1) << "dual quote: only additional -BTC is allowed";
      for (const auto& [quote, cnt] : quote_ccy_count_) {
        (void)cnt;
        if (quote != "BTC") {
          spot_pnl_currency_ = quote;
        }
      }
    } else {
      spot_pnl_currency_ = product.quote().currency();
    }
    {
      base_to_quotep_[std::make_pair(product.base().currency(), product.quote().currency())] =
          std::optional<double>();
    }

    if (reserve_map_.count(product.quote().currency()) == 0) {
      reserve_map_[product.quote().currency()] = 0;
    }
  }

  void UpdateMidp(const IProduct& product, std::optional<double> midp) {
    std::pair<std::string, std::string> ccypair =
        std::make_pair(product.base().currency(), product.quote().currency());

    if (base_to_quotep_.count(ccypair) > 0) {
      base_to_quotep_.at(ccypair) = midp;
    }

    if (midp.has_value()) {
      strat_logger_->UpdateMidp(product.order_native_symbol(), midp.value());

      {
        std::lock_guard<std::mutex> lock(ticker_mutex_);
        const auto& norm_product = product.absolute_norm();
        auto& ticker = ticker_info_map_[norm_product];
        ticker.set_timestamp(GetCurrentTimestamp());
        ticker.set_mea(product.market_name() + "." + product.exchange_name());
        ticker.set_norm_product(norm_product);
        ticker.set_native_product(product.order_native_symbol());
        ticker.set_midp(midp.value());
      }
    }
  }

  bool GetTickerInfo(const std::string& norm_product, TickerInfo* ticker_info) {
    std::unique_lock<std::mutex> lock(ticker_mutex_, std::defer_lock);

    if (!lock.try_lock()) {
      return false;
    }

    if (ticker_info_map_.count(norm_product) == 0) {
      return false;
    }

    ticker_info->CopyFrom(ticker_info_map_.at(norm_product));
    ticker_info->set_timestamp(GetCurrentTimestamp());
    return true;
  }

  void onAccountInfo(const exchange::base::order::OrderUpdate& upd) {
    // For monitor
    auto curr_time = GetCurrentTimestamp();
    if (prev_pnl_send_time_ + pnl_interval_ < curr_time) {
      if (strategy_config_.has_reporter_config()) {
        strat_logger_->WriteStratReporterConfig(
            strategy_config_.reporter_config(), true, curr_time);
      }
      LOG(INFO) << upd.mea().String();
      if (upd.mea().market == MarketType::Futures || upd.mea().market == MarketType::Options) {
        LOG(INFO) << "********************";
        for (const auto& it : upd.account_info().GetPositions()) {
          LOG(INFO) << "* " << it.second.symbol() << ": " << it.second.net_position();
        }
        LOG(INFO) << "********************";
        LOG(INFO) << "####################";
        for (const auto& it : upd.account_info().GetBalances()) {
          double total = it.second.total();
          if (reserve_map_.count(it.second.currency()) > 0) {
            total -= reserve_map_.at(it.second.currency());
          }
          if (it.second.currency().length() > 0) {
            LOG(INFO) << "# " << it.second.currency() << ": " << it.second.total();
            pnl_balance_buf_.Clear();
            pnl_balance_buf_.set_currency(it.second.currency());
            pnl_balance_buf_.set_total(total);
            strat_logger_->WritePnlBalance(pnl_balance_buf_, std::nullopt, true, curr_time);
          }
        }
        LOG(INFO) << "####################";
      } else if (upd.mea().market == MarketType::Spot) {
        // Write reserve periodically
        if (acc_balance_for_reserve_.each_balance_size() > 0) {
          strat_logger_->WriteReserve(acc_balance_for_reserve_, true, curr_time);
        }

        std::unordered_map<std::string, double> balance_map;
        std::unordered_map<std::string, double> nav_balance_map;
        LOG(INFO) << "####################";
        for (const auto& it : upd.account_info().GetBalances()) {
          if (it.second.currency().length() > 0) {
            LOG(INFO) << "# " << it.second.currency() << ": " << it.second.total();
            balance_map[it.second.currency()] = it.second.total();
            nav_balance_map[it.second.currency()] = it.second.total();
          }
        }
        for (auto& [currency, reserve] : reserve_map_) {
          balance_map[currency] -= reserve;
        }
        LOG(INFO) << "####################";

        bool has_midp_all = true;
        std::unordered_map<std::string, std::pair<double, double>> pnl_map;
        for (auto [currency_pair, midp] : base_to_quotep_) {
          std::string base_currency = currency_pair.first;
          // we don't calculate exotic currency
          if (reserve_map_.count(base_currency) == 0) {
            continue;
          }

          // -BTC case
          // ex: BNB-BTC -> BNB-BTC * BTC-USDT = BNB-USDT
          if (currency_pair.second != spot_pnl_currency_) {
            auto convertprod = std::make_pair(currency_pair.second, spot_pnl_currency_);
            CHECK_EQ(base_to_quotep_.count(convertprod), 1)
                << "Add " << convertprod.first << "-" << convertprod.second;
            std::optional<double> multip2 = base_to_quotep_.at(convertprod);
            if (!midp.has_value()) {
            } else if (!multip2.has_value()) {
              LOG(INFO) << "NO CHAIN MIDP: " << convertprod.first << "-" << convertprod.second;
              continue;
            } else {
              *midp = midp.value() * multip2.value();
            }
          }

          if (!midp.has_value()) {
            has_midp_all = false;
            LOG(INFO) << "NO MIDP : " << base_currency;
            continue;
          }
          pnl_map[spot_pnl_currency_].first += balance_map[base_currency] * midp.value();
          pnl_map[spot_pnl_currency_].second += nav_balance_map[base_currency] * midp.value();
          LOG(INFO) << "# " << base_currency << " pnlbal: " << balance_map[base_currency] << " in "
                    << spot_pnl_currency_ << " (x " << midp.value() << ")"
                    << " = " << balance_map[base_currency] * midp.value();
          LOG(INFO) << "# " << base_currency << " navbal: " << nav_balance_map[base_currency]
                    << " in " << spot_pnl_currency_ << " (x " << midp.value() << ")"
                    << " = " << nav_balance_map[base_currency] * midp.value();
        }
        if (has_midp_all) {
          for (auto& [currency, balance_tuple] : pnl_map) {
            auto& [balance, nav_balance] = balance_tuple;
            if (currency.length() > 0) {
              pnl_balance_buf_.Clear();
              pnl_balance_buf_.set_currency(currency);
              // We should existed quote balance like USDT, KRW.
              pnl_balance_buf_.set_total(balance + balance_map[currency]);
              coin::proto::CurrencyBalance nav_balance_buf;
              nav_balance_buf.set_currency(currency);
              nav_balance_buf.set_total(nav_balance + nav_balance_map[currency]);
              strat_logger_->WritePnlBalance(pnl_balance_buf_, nav_balance_buf, true, curr_time);
              LOG(INFO) << "## " << currency << " pnlbal: " << pnl_balance_buf_.total();
              LOG(INFO) << "## " << currency << " navbal: " << nav_balance_buf.total();
            }
          }
          LOG(INFO) << "HAVE midp";
          LOG(INFO) << pnl_balance_buf_.DebugString();
        } else {
          LOG(WARNING) << "DOESN't HAVE midp";
        }
      } else {
        THROW();
      }
      prev_pnl_send_time_ = curr_time;
    }
  }

 private:
  const StrategyManagerConfig strategy_config_;
  std::unique_ptr<StrategyLogger> strat_logger_;
  std::unique_ptr<StrategySlimLogger> strat_slim_logger_;
  std::unique_ptr<TelemetryLogger> telemetry_logger_;
  std::unordered_map<std::string, double> reserve_map_;

  std::mutex ticker_mutex_;
  std::map<std::pair<std::string, std::string>, std::optional<double>> base_to_quotep_;
  std::unordered_map<std::string, TickerInfo> ticker_info_map_;

  std::string spot_pnl_currency_;
  std::unordered_map<std::string, int> base_ccy_count_;
  std::unordered_map<std::string, int> quote_ccy_count_;
  coin::proto::CurrencyBalance pnl_balance_buf_;
  coin::proto::AccountBalance acc_balance_for_reserve_;
  int64_t prev_pnl_send_time_;
  int64_t pnl_interval_;
};

}  // namespace coin2::strategy::util
